#include <Render/Include/RendererDX9.h>

#include <Render/Include/MeshDX9.h>
#include <Render/Include/TextureDX9.h>

#include <D3dx9shape.h>

using namespace Render;

// ========== LIFECYCLE ========== //

RendererDX9::RendererDX9(IWinWindow* window)
    :Renderer()
{
    LPDIRECT3D9 p_d3d9;
    D3DDISPLAYMODE d3ddm;
    
    // Create the D3D object.
    if (NULL == (p_d3d9 = Direct3DCreate9(D3D_SDK_VERSION)))
    {
        throw -1;
    }

    // Get the current desktop display mode, so we can set up a back
    // buffer of the same format
    if (FAILED(p_d3d9->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
    {
        throw -1;
    }

    D3DPRESENT_PARAMETERS d3dpp; 
    ZeroMemory(&d3dpp, sizeof(d3dpp));

    d3dpp.Windowed = TRUE;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
    // We're using the current display mode. 
    d3dpp.BackBufferFormat = d3ddm.Format;
    d3dpp.BackBufferWidth = window->getWidth();
    d3dpp.BackBufferHeight = window->getHeight();

    d3dpp.EnableAutoDepthStencil = TRUE;
    d3dpp.AutoDepthStencilFormat = D3DFMT_D32;
    HRESULT hr = p_d3d9->CheckDepthStencilMatch(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, d3ddm.Format, d3ddm.Format, D3DFMT_D32);

    if (FAILED (hr))
    {
        d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
    }

    // Create the D3DDevice
    if (FAILED(p_d3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, window->getHWND(), D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &(this->p_device))))
    {
        throw -1;
    }

    /// Creates and sets the viewport
    D3DVIEWPORT9 vp;
    vp.Width = window->getWidth();
    vp.Height = window->getHeight();
    vp.MinZ = 0.0f;
    vp.MaxZ = 1.0f;
    vp.X = 0;
    vp.Y = 0;
    this->p_device->SetViewport(&vp);

    // Turn on the zbuffer
    this->p_device->SetRenderState(D3DRS_ZENABLE, TRUE);

    
    // create the torus effect
    LPD3DXBUFFER errors;
    // hr = D3DXCreateEffectFromFile( this->p_device, L"..\\Render\\Shader\\DX9Effect.fx", NULL, NULL, NULL, NULL, &this->effect, &errors );
    hr = D3DXCreateEffectFromFile( this->p_device, L"DX9Effect.fx", NULL, NULL, NULL, NULL, &this->effect, &errors );
    if (FAILED (hr))
    {
          char* auxi = (char*)errors->GetBufferPointer();
          auxi = 0;
    }
    
}

RendererDX9::~RendererDX9(void)
{
    this->p_device->Release();
}

// ========== OVERLODED OPERATORS ========== //

// ========== METHODS ========== //

void RendererDX9::renderTorus(AppModel::Torus* torus)
{
    HRESULT hr;

    hr = this->p_device->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, (D3DCOLOR)0x00000000, 1.0f, 0x00000000);
    if (D3D_OK == this->p_device->BeginScene()) 
    {
        MeshDX9* mesh = dynamic_cast<MeshDX9*>(torus->getMesh());
        TextureDX9* texture = dynamic_cast<TextureDX9*>(torus->getTexture());
        // set the vertex buffer
        
        /// set the transformation matrix for the element
        D3DXMATRIX world_matrix;
        D3DXMatrixIdentity(&world_matrix);
        this->effect->SetMatrix( "world", &world_matrix );
                
        D3DXMATRIX view_matrix;
        D3DXVECTOR3 camera_position;
        D3DXVECTOR3 camera_at;
        D3DXVECTOR3 camera_up;
        float camera_pos_x = this->arc_ball_camera_radio * cos(this->arc_ball_camera_theta) * cos (this->arc_ball_camera_phi);
        float camera_pos_y = this->arc_ball_camera_radio * sin(this->arc_ball_camera_theta) * cos (this->arc_ball_camera_phi);
        float camera_pos_z = this->arc_ball_camera_radio * sin (this->arc_ball_camera_phi);
        camera_position = D3DXVECTOR3( camera_pos_x, camera_pos_y, camera_pos_z);
        camera_at = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
        camera_up = D3DXVECTOR3(0.0f, 0.0f, 1.0f);

        D3DXMatrixLookAtLH(&view_matrix, &camera_position, &camera_at, &camera_up);
        this->effect->SetMatrix( "view", &view_matrix );

        /// set the projection
        D3DXMATRIX projection_matrix;
        D3DXMatrixPerspectiveFovLH(&projection_matrix, 3.14f/3.0f, 1.0f, 1.0f, 100.0f);
        this->effect->SetMatrix( "projection", &projection_matrix ); 

        D3DXMATRIX normal_matrix;
        D3DXMatrixMultiply(&normal_matrix, &world_matrix, &view_matrix);
        float determinant;
        D3DXMatrixInverse(&normal_matrix, &determinant, &normal_matrix);
        D3DXMatrixTranspose(&normal_matrix, &normal_matrix);
        this->effect->SetMatrix( "normal_matrix", &normal_matrix ); 

        this->effect->SetFloat( "wave_offset", torus->getWaveOffset() );
        
        D3DXVECTOR4 ambient_material_color = D3DXVECTOR4(0.05f, 0.05f, 0.05f, 1.0f);
        D3DXVECTOR4 diffuse_material_color = D3DXVECTOR4(0.45f, 0.45f, 0.45f, 1.0f);
        D3DXVECTOR4 specular_material_color = D3DXVECTOR4(1.0f, 1.0f, 1.0f, 1.0f);
        D3DXVECTOR4 light_color = D3DXVECTOR4(1.0f, 1.0f, 1.0f, 1.0f);
        D3DXVECTOR3 light_dir = D3DXVECTOR3(-1.0f, -1.0f, -1.0f);//-camera_position;
        float material_specular_power = 10.0f;

        this->effect->SetFloatArray("ambient_material_color", (float*)&ambient_material_color, 4);
        this->effect->SetFloatArray("diffuse_material_color", (float*)&diffuse_material_color, 4);
        this->effect->SetFloatArray("specular_material_color", (float*)&specular_material_color, 4);
        this->effect->SetFloatArray("light_color", (float*)&light_color, 4);
        this->effect->SetFloatArray("light_dir", (float*)&light_dir, 3);
        this->effect->SetFloat( "material_specular_power", material_specular_power );
        
        this->effect->SetTexture( "torus_texture", texture->getTexture() );
        
        this->effect->SetTechnique( "RenderTechnique" );
        UINT cPasses;
        this->effect->Begin( &cPasses, 0 );
        for( UINT iPass = 0; iPass < cPasses; iPass++ )
        {
            this->effect->BeginPass( iPass );
            mesh->getLPD3DXMESH()->DrawSubset(0);            
            this->effect->EndPass();
        }
        this->effect->End();

        
        this->p_device->EndScene();
    }
    
    this->p_device->Present(NULL, NULL, NULL, NULL);    
}

Mesh* RendererDX9::createMesh(VertexData* vertex_data, const unsigned int &vertex_count, IndexData* index_data, const unsigned int &index_count)
{
    HRESULT hr;

    DWORD faces = index_count / 3;
    DWORD vertex = vertex_count;

    /// 3 components for the layout, 1 more to finish it
    D3DVERTEXELEMENT9 layout[4];
    unsigned int vertex_info_size = 0;
    
    /// first position
    layout[0].Stream = 0;
    layout[0].Offset = 0;
    layout[0].Type = D3DDECLTYPE_FLOAT3;
    layout[0].Method = D3DDECLMETHOD_DEFAULT;
    layout[0].Usage = D3DDECLUSAGE_POSITION;
    layout[0].UsageIndex = 0;
    
    vertex_info_size += sizeof(float) * 3;
    
    /// second normal
    layout[1].Stream = 0;
    layout[1].Offset = vertex_info_size;
    layout[1].Type = D3DDECLTYPE_FLOAT3;
    layout[1].Method = D3DDECLMETHOD_DEFAULT;
    layout[1].Usage = D3DDECLUSAGE_NORMAL;
    layout[1].UsageIndex = 0;
    
    vertex_info_size += sizeof(float) * 3;

    // texture coordinates
    layout[2].Stream = 0;
    layout[2].Offset = vertex_info_size;
    layout[2].Type = D3DDECLTYPE_FLOAT2;
    layout[2].Method = D3DDECLMETHOD_DEFAULT;
    layout[2].Usage = D3DDECLUSAGE_TEXCOORD;
    layout[2].UsageIndex = 0;
    vertex_info_size += sizeof(float) * 2;

    // // This is used to initialize the last vertex element in a vertex declaration array 
    // #define D3DDECL_END() {0xFF, 0, D3DDECLTYPE_UNUSED, 0, 0, 0}
    layout[3].Stream = 0xFF;
    layout[3].Offset = 0;
    layout[3].Type = D3DDECLTYPE_UNUSED;
    layout[3].Method = 0;
    layout[3].Usage = 0;
    layout[3].UsageIndex = 0;

    ID3DXMesh* mesh;
    hr = D3DXCreateMesh(faces, vertex, D3DXMESH_SYSTEMMEM | D3DXMESH_32BIT, layout, p_device, &mesh);
    if (FAILED(hr))
    {
        throw -1;
    }
    
    /// Load vertex data
    void* ptr_vertex_buffer_data;
    mesh->LockVertexBuffer(0, &ptr_vertex_buffer_data);

    unsigned char* aux_byte_ptr = (unsigned char*) ptr_vertex_buffer_data;
    memcpy(aux_byte_ptr, (void*)vertex_data, vertex_count * sizeof(VertexData));

    mesh->UnlockVertexBuffer();

    /// Load index data
    void* ptr_index_buffer_data;
    mesh->LockIndexBuffer(0, &ptr_index_buffer_data);
    
    unsigned char* aux_byte_ptr_index = (unsigned char*) ptr_index_buffer_data;
    memcpy(aux_byte_ptr_index, (void*)index_data, index_count * sizeof(IndexData));
        
    mesh->UnlockIndexBuffer();

    /// There is only one submesh with 1 material or attribute, the 0
    DWORD* ptr_att_buffer;
    mesh->LockAttributeBuffer(0, &ptr_att_buffer);
    for (unsigned int n = 0; n < faces; n++)
    {
        *(ptr_att_buffer + n) = 0;
    }
    mesh->UnlockAttributeBuffer();
        
    MeshDX9* mesh_dx9 = new MeshDX9(mesh);

    return mesh_dx9;
}

Texture* RendererDX9::createTexture(const char* file)
{
    LPDIRECT3DTEXTURE9 p3DTexture;
    D3DXIMAGE_INFO texture_info;
    HRESULT hr;

    std::wstring unicode_text_file = this->convertToWideString(std::string(file));
    hr = D3DXCreateTextureFromFileEx(p_device, unicode_text_file.c_str(), 
        D3DX_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0, D3DFMT_UNKNOWN, 
        D3DPOOL_MANAGED, D3DX_DEFAULT, 
        D3DX_DEFAULT, 0, &texture_info, NULL, &p3DTexture);
    if (FAILED (hr))
    {
        throw -1;
    }

    TextureDX9* texture =  new TextureDX9(p3DTexture, texture_info);
    return texture;
}

// ========== GETTERS ========== //

// ========== SETTERS ========== //

// ========== INQUIRY ========== //

// ========== INTERNAL METHODS ========== //

std::wstring RendererDX9::convertToWideString(const std::string &cadena)
{
    int len;
    int slength = (int)cadena.length() + 1;
    len = MultiByteToWideChar(CP_ACP, 0, cadena.c_str(), slength, 0, 0);
    wchar_t* buf = new wchar_t[len];
    MultiByteToWideChar(CP_ACP, 0, cadena.c_str(), slength, buf, len);
    std::wstring r(buf);
    delete[] buf;
    return r;
}
